using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using HtmlAgilityPack;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Web.Framework.Clients.GitHub;
using StardewModdingAPI.Web.Framework.ConfigModels;
using StardewModdingAPI.Web.ViewModels;

namespace StardewModdingAPI.Web.Controllers;

/// <summary>Provides an info/download page about SMAPI.</summary>
[Route("")]
internal class IndexController : Controller
{
    /*********
    ** Fields
    *********/
    /// <summary>The site config settings.</summary>
    private readonly SiteConfig SiteConfig;

    /// <summary>The cache in which to store release data.</summary>
    private readonly IMemoryCache Cache;

    /// <summary>The GitHub API client.</summary>
    private readonly IGitHubClient GitHub;

    /// <summary>The cache time for release info.</summary>
    private readonly TimeSpan CacheTime = TimeSpan.FromMinutes(10);

    /// <summary>The GitHub repository name to check for update.</summary>
    private readonly string RepositoryName = "Pathoschild/SMAPI";


    /*********
    ** Public methods
    *********/
    /// <summary>Construct an instance.</summary>
    /// <param name="cache">The cache in which to store release data.</param>
    /// <param name="github">The GitHub API client.</param>
    /// <param name="siteConfig">The context config settings.</param>
    public IndexController(IMemoryCache cache, IGitHubClient github, IOptions<SiteConfig> siteConfig)
    {
        this.Cache = cache;
        this.GitHub = github;
        this.SiteConfig = siteConfig.Value;
    }

    /// <summary>Display the index page.</summary>
    [HttpGet]
    public async Task<ViewResult> Index()
    {
        // choose versions
        ReleaseVersion[] versions = await this.GetReleaseVersionsAsync();
        ReleaseVersion? stableVersion = versions.LastOrDefault(version => !version.IsForDevs);
        ReleaseVersion? stableVersionForDevs = versions.LastOrDefault(version => version.IsForDevs);

        // render view
        IndexVersionModel stableVersionModel = stableVersion != null
            ? new IndexVersionModel(stableVersion.Version.ToString(), stableVersion.Release.Body, stableVersion.Release.WebUrl, stableVersion.Asset.DownloadUrl, stableVersionForDevs?.Asset.DownloadUrl)
            : new IndexVersionModel("unknown", "", "https://github.com/Pathoschild/SMAPI/releases", "https://github.com/Pathoschild/SMAPI/releases", null); // just in case something goes wrong

        // render view
        var model = new IndexModel(stableVersionModel, this.SiteConfig.OtherBlurb, this.SiteConfig.SupporterList);
        return this.View(model);
    }

    /// <summary>Display the index page.</summary>
    [HttpGet("/privacy")]
    public ViewResult Privacy()
    {
        return this.View();
    }


    /*********
    ** Private methods
    *********/
    /// <summary>Get a sorted, parsed list of SMAPI downloads for the latest releases.</summary>
    private async Task<ReleaseVersion[]> GetReleaseVersionsAsync()
    {
        ReleaseVersion[]? versions = await this.Cache.GetOrCreateAsync("available-versions", async entry =>
        {
            entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime);

            // get latest stable release
            GitRelease? release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false);

            // strip 'noinclude' blocks from release description
            if (release != null)
            {
                HtmlDocument doc = new();
                doc.LoadHtml(release.Body);
                foreach (HtmlNode node in doc.DocumentNode.SelectNodes("//*[@class='noinclude']")?.ToArray() ?? [])
                    node.Remove();
                release.Body = doc.DocumentNode.InnerHtml.Trim();
            }

            // get versions
            return this
                .ParseReleaseVersions(release)
                .OrderBy(p => p.Version)
                .ToArray();
        });

        return versions!; // GetOrCreateAsync doesn't return null unless we provide null in the callback
    }

    /// <summary>Get a parsed list of SMAPI downloads for a release.</summary>
    /// <param name="release">The GitHub release.</param>
    private IEnumerable<ReleaseVersion> ParseReleaseVersions(GitRelease? release)
    {
        if (release?.Assets == null)
            yield break;

        foreach (GitAsset asset in release.Assets)
        {
            if (asset.FileName.StartsWith("Z_"))
                continue;

            Match match = Regex.Match(asset.FileName, @"SMAPI-(?<version>[\d\.]+(?:-.+)?)-installer(?<forDevs>-for-developers)?.zip");
            if (!match.Success || !SemanticVersion.TryParse(match.Groups["version"].Value, out ISemanticVersion? version))
                continue;
            bool isForDevs = match.Groups["forDevs"].Success;

            yield return new ReleaseVersion(release, asset, version, isForDevs);
        }
    }

    /// <summary>A parsed release download.</summary>
    private class ReleaseVersion
    {
        /*********
        ** Accessors
        *********/
        /// <summary>The underlying GitHub release.</summary>
        public GitRelease Release { get; }

        /// <summary>The underlying download asset.</summary>
        public GitAsset Asset { get; }

        /// <summary>The SMAPI version.</summary>
        public ISemanticVersion Version { get; }

        /// <summary>Whether this is a 'for developers' download.</summary>
        public bool IsForDevs { get; }


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name="release">The underlying GitHub release.</param>
        /// <param name="asset">The underlying download asset.</param>
        /// <param name="version">The SMAPI version.</param>
        /// <param name="isForDevs">Whether this is a 'for developers' download.</param>
        public ReleaseVersion(GitRelease release, GitAsset asset, ISemanticVersion version, bool isForDevs)
        {
            this.Release = release;
            this.Asset = asset;
            this.Version = version;
            this.IsForDevs = isForDevs;
        }
    }
}
